import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Random;


/**
 * Authors - Oren Bracha, Rony Lahav and Yehuda Eterman
 * 
 * the Class handles the data receiving side of the application in UDP
 * sending request entry, receiving entries and updates 
 */
public class UDPReceiverConnection implements Runnable{
	private DatagramSocket concreteSocket; // Will hold a reference for the specific socket
	private String senderName;
	private String senderIP;
	private int senderPort;
	
	private int inDebthForRcvWin = 0;
	boolean stopRequesting = false;
	
	String dbVersionOfSendingPeer;

	DatagramPacket firstPacket;
	private RequestsManager reqManager = null;
	
	private RequestsManager picReqManager = null;
	
	long uniqueID;
	
	String connType = "udp";
	/**
	 * initializing data
	 * @param uniqueID
	 * @param senderPort
	 * @param rcvPacket
	 * @param senderIP
	 * @throws SocketException
	 */
	public UDPReceiverConnection(long uniqueID, int senderPort, DatagramPacket rcvPacket, String senderIP) throws SocketException
	{
		this.senderIP = senderIP;
		this.senderPort = senderPort;
		firstPacket = rcvPacket;
		concreteSocket = new DatagramSocket();
		concreteSocket.setSoTimeout(proj350.TIMEOUT_IDLE_PERIOD);
		this.uniqueID = uniqueID;
	}
	
	public void run()
	{
		try{
			String firstMessage = new String(firstPacket.getData(),0,firstPacket.getLength());
			
			// Wait for an update message until I get one. null will return If I didn't get one
			HashMap<String, String> parsedMessage = MessageCenter.getInstance().parseValidMessage(firstMessage, null, firstPacket.getAddress().getHostAddress(), Integer.toString(firstPacket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), false);
			
			if (parsedMessage != null){
				//checking that the first message is indeed a proper UPDATE message
				if (parsedMessage.get(MessageCenter.TYPE_MESSAGE).compareTo(MessageCenter.TYPE_UPDATE) == 0){
					senderName = parsedMessage.get(MessageCenter.VAR_PEERNAME);
					dbVersionOfSendingPeer = parsedMessage.get(MessageCenter.VAR_DBVERSION);
					//updating that the opposing peer is online and connected
					DataBase.getInstance().setPeerOnline(senderName);
					DataBase.getInstance().setLastConnectionTime(senderName);

					Logger.getInstance().printInOutMsg(true, firstMessage, null, senderIP, Integer.toString(firstPacket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(proj350.getMyUDPPort()), connType);
					
					//send a request entry if needed
					boolean reqEntryStepSucc = HandleReqEntrySessionIfNeeded(parsedMessage);
					
					//if we succeeded in receiving the entry
					if (reqEntryStepSucc){
						if (reqManager != null){
							//get the entries from the messages
							DBTuple[] dbTuples = reqManager.getEntries();
							String zeroDate = DateStringConverter.getZeroDateAsString();
							//update all the peers that arrived
							for (DBTuple tuple : dbTuples) {
								DBTuple tupleFromDB = DataBase.getInstance().findTuple(tuple.getPeerName());
								boolean isZeroDated = false;
								int isNewerPicVersion = 0;
								String peerName = null;
								if (tupleFromDB != null){
									peerName = tupleFromDB.getPeerName();
									isZeroDated = tupleFromDB.getPeerCommitTime().equals(zeroDate);
									isNewerPicVersion = DateStringConverter.compare2Dates(tuple.getPicVersion(), tupleFromDB.getPicVersion());
								}
								else {
									peerName = tuple.getPeerName();
									isNewerPicVersion = 1;
									isZeroDated = true;
								}
								DataBase.getInstance().updateTupleIfRelevant(tuple);
								
								//if peer doesn't have an updated pic the peerVersionDeclared is 0-dated
								//if the new picVersion is bigger than the old PicVersion we initialize a new picture procedure for the peer
								if ((isZeroDated && isNewerPicVersion>=0) || 
									(!isZeroDated && isNewerPicVersion>0)){
									DataBase.getInstance().addPeerThatHasNewPic(peerName, senderName);
								}
							}							
						}
						String[] peersForReqPic = DataBase.getInstance().findPeersThatHavePeerInPicSet(senderName);
						
						if (peersForReqPic != null){	//begin request pictures sequence
							Random rand = new Random(System.currentTimeMillis());

							//request picture for the peers that I do not have a current picture of
							for (String peerForReqPic : peersForReqPic) {
								//load balancing - request picture only if we pass the probability test
								if (DataBase.getInstance().findProbabiltyForPictureUpdate(peerForReqPic)>rand.nextFloat()){
									DBTuple tuple = DataBase.getInstance().findTuple(peerForReqPic);
									//find the next segment we need for the picture
									String firstSegmentNumber = DataBase.getInstance().getNumberOfNextPicSegment(peerForReqPic);
									String dbVersion = tuple.getDbVersion();
									//allocate receive window as large as we can for the picture
									int rcvWinSpace = PictureHandler.allocateRcvWin(Integer.parseInt(tuple.getPicSize())-proj350.getMSS()/2*(Integer.parseInt(firstSegmentNumber)-1));
									inDebthForRcvWin += rcvWinSpace;
									if (rcvWinSpace !=0){
										rcvWinSpace*=2; // Data will arrive encoded in hexadecimal format - twice times bits
										int numOfFragments = rcvWinSpace/proj350.getMSS() + (rcvWinSpace%proj350.getMSS()==0?0:1);
										picReqManager = new RequestsManager(numOfFragments,Integer.parseInt(firstSegmentNumber));
										
										//find the last picture fragment that we will receive
										String lastSegmentNumber = Integer.toString((Integer.parseInt(firstSegmentNumber)+numOfFragments-1));
										//prepare a request picture message with range
										String requestPictureMsg = MessageCenter.getInstance().createMessage_RequestPicture_DB_RCV_Peer_Range(dbVersion, Integer.toString(rcvWinSpace), tuple.getPeerName(), firstSegmentNumber, lastSegmentNumber);
										//request a range of picture fragments and get the response
										boolean reqPicStepSucc = HandleReqPictureSession(requestPictureMsg,peerForReqPic,Integer.parseInt(firstSegmentNumber)-1);
										
										//check if we received the picture properly
										if (reqPicStepSucc && 
											Integer.parseInt(firstSegmentNumber) == Integer.parseInt(DataBase.getInstance().getNumberOfNextPicSegment(peerForReqPic))){
											String[] fragments = picReqManager.getPicture();
											//add the fragments that we received to the DB
											DataBase.getInstance().addSegmentsToPicture(tuple.getPeerName(), fragments);
										}
										
										//this is a check for event that the picture has changed  before we updated the
										// the pic into the DB
										if (!stopRequesting){
											//if we got the whole picture we insert it into the DB
											if (DataBase.getInstance().isPictureCompletelyReceived(tuple.getPeerName())){
												PictureHandler.combinePictureFromParts(tuple.getPeerName());
												
												String currTime = DateStringConverter.timeCurrentForMessages();
												tuple.setPeerCommitTime(currTime);
												DBTuple myTuple = proj350.getMyTuple();
												myTuple.setDbVersion(currTime);
												myTuple.setPeerCommitTime(currTime);
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}
		catch (IOException e){
		}
		catch (Exception e2){
		}
		finally{
			try{
				if (inDebthForRcvWin > 0){
					proj350.getInstance().freeRcvWinSpace(inDebthForRcvWin);
				}
				
				proj350.removeThreadFromLivingList(uniqueID);
				concreteSocket.close();
			}
			catch (Exception e){
			}
			
		}
	}
	
	/**
	 * the function sends request picture and receives in response a picture message
	 * @param reqPictureMessage
	 * @param peerForReqPic
	 * @param firstSegNum
	 * @return true if action was successful
	 * @throws IOException
	 */
	public boolean HandleReqPictureSession(String reqPictureMessage, String peerForReqPic, int firstSegNum) throws IOException{
		String requestPictureMessage = null;
		boolean stillValid = true;
		//send request picture
		if (requestPicturesFirstTime(reqPictureMessage,firstSegNum)){
			while (!picReqManager.isComplete() && stillValid && !stopRequesting){
				//find if there are missing fragments that disappeared
				String missingFragments = picReqManager.getMissingFragmentsList();
				HashMap<Integer, String> incIdToGlobIdMap = mapNewRequestsToSerials(missingFragments);
			
				//send request picture for the fragments we lost in the middle of transport
				requestPictureMessage = MessageCenter.getInstance().createMessage_RequestPicture_DB_RCV_Peer_List(dbVersionOfSendingPeer, Integer.toString(inDebthForRcvWin*2), peerForReqPic, missingFragments);
				sendMessage(requestPictureMessage);
				Logger.getInstance().printInOutMsg(false, requestPictureMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, senderIP, Integer.toString(senderPort),connType);
				if (!waitForPictures(incIdToGlobIdMap))
				{
					stillValid = false;
				}
			}
		}
		else
		{
			stillValid = false;
		}
		
		return (stillValid && !stopRequesting);
	}
	
	/**
	 * a function to request when the peer first loads
	 * @param reqPictureMessage
	 * @param firstSegNum
	 * @return true if receive successful
	 * @throws IOException
	 */
	public boolean requestPicturesFirstTime(String reqPictureMessage, int firstSegNum) throws IOException{
		//send request picture message and print it to log
		sendMessage(reqPictureMessage);
		Logger.getInstance().printInOutMsg(false, reqPictureMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, senderIP, Integer.toString(firstPacket.getPort()),connType);
		
		boolean firstPictureAccepted = false;
		
		// Wait for the first PICTURE message to determine the number of picture should expect
		HashMap<String, String> firstValidPictureMessage = null;
		do{
			// Wait for picture message
			firstValidPictureMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_PICTURE);
			
			if (firstValidPictureMessage != null){
				//check if the picture we receive is obsolete
				stopRequesting = checkExpiartionOfPicMsg(firstValidPictureMessage);
				
				if (!stopRequesting){
					firstPictureAccepted = true;

					//free the receive window by the data we received
					int pictureDatasize = firstValidPictureMessage.get(MessageCenter.VAR_DATA).length();
					proj350.getInstance().freeRcvWinSpace(pictureDatasize/2);
					inDebthForRcvWin -= pictureDatasize/2;
					int sn = Integer.parseInt(firstValidPictureMessage.get(MessageCenter.VAR_SN))+firstSegNum;
					//update picture fragments and handle 
					picReqManager.updateEntry(Integer.parseInt(firstValidPictureMessage.get(MessageCenter.VAR_SN)), Integer.toString(sn), null, firstValidPictureMessage.get(MessageCenter.VAR_DATA), true);	
				}
			}
		} while (firstValidPictureMessage != null && !picReqManager.isComplete() && !stopRequesting);
				
		return firstPictureAccepted;
	}
	
	/**
	 * sending request entry and receiving entries
	 * @param parsedMessage
	 * @return true if success
	 * @throws IOException
	 */
	public boolean HandleReqEntrySessionIfNeeded(HashMap<String, String> parsedMessage) throws IOException{
		String peerSenderDBVersion = parsedMessage.get(MessageCenter.VAR_DBVERSION);
		
		DBTuple senderTupleDetails = DataBase.getInstance().findTuple(senderName);
		
		boolean needEntries = false;
		boolean stillValid = true; 
		String requestEntryMessage = null;
		String dbVersionRequested = null;
		if (senderTupleDetails != null)
		{
			//check if need entries
			if (DateStringConverter.getMSFromStringMessageFormat(senderTupleDetails.getDbVersion()) <
				DateStringConverter.getMSFromStringMessageFormat(peerSenderDBVersion)) {
				requestEntryMessage = MessageCenter.getInstance().createMessage_RequestEntry_DB(senderTupleDetails.getDbVersion());
				dbVersionRequested = senderTupleDetails.getDbVersion();
				needEntries = true;
			}
		}
		else
		{
			//create message that requests all entries 
			dbVersionRequested = "0";
			requestEntryMessage = MessageCenter.getInstance().createMessage_RequestEntry_DB("0");
			needEntries = true;
		}	
		
		//prepare requestEntry for every peer that needs it
		if (needEntries && requestEntriesFirstTime(requestEntryMessage)){
			while (!reqManager.isComplete() && stillValid){
				LinkedList<String[]> missingEntries = reqManager.getRangesList();
				for (String[] range : missingEntries) {
					//create message and send it
					requestEntryMessage = MessageCenter.getInstance().createMessage_RequestEntry_DB_Range(dbVersionRequested, range[0], range[1]);
					sendMessage(requestEntryMessage);
					Logger.getInstance().printInOutMsg(false, requestEntryMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, senderIP, Integer.toString(senderPort),connType);
				}
				if (!waitForEntries())
				{
					stillValid = false;
				}
			}
		}
		else
		{
			if (needEntries){
				stillValid = false;
			}
		}
		
		return stillValid;
	}
	
	/**
	 * sends message
	 * @param message
	 * @throws IOException
	 */
	private void sendMessage(String message) throws IOException{
		DatagramPacket dp = new DatagramPacket(message.getBytes(),message.length(),InetAddress.getByName(senderIP),senderPort);
		concreteSocket.send(dp);
	}
	
	/**
	 * read message
	 * @return the packet of the message
	 */
	private DatagramPacket readMessage() {
		try{
			byte[] buffer = new byte[proj350.getMSS()*2];
			DatagramPacket dp = new DatagramPacket(buffer,buffer.length);
			concreteSocket.receive(dp);
			return dp;
		}
		catch (IOException e){
			return null;
			//throw e;
		}
	}
	
	private String getPacketAsString(DatagramPacket dp){
		return (new String(dp.getData(),0,dp.getLength())); 
	}
	
	/**
	 * 
	 * This method perform the requestentry/entry for the first time to determine how many entries
	 * I need to get from the sender
	 */
	public boolean requestEntriesFirstTime(String reqEntryMessage) throws IOException {
		sendMessage(reqEntryMessage);
		
		Logger.getInstance().printInOutMsg(false, reqEntryMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, senderIP, Integer.toString(firstPacket.getPort()),connType);
		
		boolean firstEntryAccepted = false;
		
		// Wait for the first ENTRY message to determine the number of entries I should expect
		HashMap<String, String> firstValidEntryMessage = null;
		do{
			// Wait for entry message 
			firstValidEntryMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_ENTRY);
			
			if (firstValidEntryMessage != null){
				firstEntryAccepted = true;
				
				// Get the number of expected entries
				int numExpectedEntries = Integer.parseInt(firstValidEntryMessage.get(MessageCenter.VAR_N));
				
				DBTuple tuple = createDBTupleFromEntryMessage(firstValidEntryMessage);
				
				// Fill the request manager with the accepted entries
				if (reqManager == null){
					reqManager = new RequestsManager(numExpectedEntries,0);
				}
				
				reqManager.updateEntry(Integer.parseInt(firstValidEntryMessage.get(MessageCenter.VAR_SN)), firstValidEntryMessage.get(MessageCenter.VAR_PEERNAME), tuple, null, true);	
			}
		} while (firstValidEntryMessage != null && !reqManager.isComplete());
		
		
		return firstEntryAccepted;
	}
	
	/**
	 * 
	 * This method gets a message type and wait to read from the socket until a message of this
	 * type is accepted (or a timeout reached)
	 */	
	public HashMap<String, String> waitForSingleSpecificMessage(String messageType){
		HashMap<String, String> parsedMessage = null;
			boolean isValidMessage = false;
			String messageRead = null;
			do {
				//read message again if it has disappeared
				DatagramPacket dp = readMessage();
				if (dp != null){
					messageRead = getPacketAsString(dp);
				}
				else
				{
					messageRead = null;
				}
				
				// Parse the message
				if (messageRead != null){
					parsedMessage= MessageCenter.getInstance().parseValidMessage(messageRead, senderName, senderIP, Integer.toString(dp.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()),true);
					if (parsedMessage != null){
						// Check if the accepted message is the type I want
						isValidMessage = (parsedMessage.get(MessageCenter.TYPE_MESSAGE).compareTo(messageType) == 0);
						if (!isValidMessage){
							Logger.getInstance().printErrorWrongFormat(messageType, messageRead, senderName, senderIP, Integer.toString(dp.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()),connType);
						}
						else
						{
							Logger.getInstance().printInOutMsg(true, messageRead, senderName, senderIP, Integer.toString(dp.getPort()),proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), connType);
						}
					}
				}
			// Continue to read until you get the type of message you want or socket was closed 
			} while(!isValidMessage && messageRead != null);
			
			return parsedMessage;
	}
	
	/**
	 * 
	 * this method wait for entries message and handles them
	 */
	public boolean waitForEntries() {
		HashMap<String, String> parsedMessage = null;
		
		boolean atLeastOneAccepted = false;
		
		do {
			// Wait for entry message
			parsedMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_ENTRY);

			//update entry if it is valid
			if (parsedMessage!=null){
				atLeastOneAccepted = true;
				DBTuple dbTuple = createDBTupleFromEntryMessage(parsedMessage);
				reqManager.updateEntry(Integer.parseInt(parsedMessage.get(MessageCenter.VAR_SN)), dbTuple.getPeerName(), dbTuple, null, false);
			}		
		} while (parsedMessage!=null);
		
		return atLeastOneAccepted;
	}
	
	/**
	 * receiving entry pictures
	 * @param map
	 * @return
	 */
	public boolean waitForPictures(HashMap<Integer, String> map) {
		HashMap<String, String> parsedMessage = null;
		
		boolean atLeastOneAccepted = false;
		
		do {
			// Wait for entry message
			parsedMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_PICTURE);
				
			if (parsedMessage!=null){
				stopRequesting = checkExpiartionOfPicMsg(parsedMessage);
				if (!stopRequesting){
					atLeastOneAccepted = true;
					int dataSize = parsedMessage.get(MessageCenter.VAR_DATA).length();
					proj350.getInstance().freeRcvWinSpace(dataSize/2);
					inDebthForRcvWin -= dataSize/2;
					picReqManager.updateEntry(Integer.parseInt(parsedMessage.get(MessageCenter.VAR_SN)), map.get(Integer.parseInt(parsedMessage.get(MessageCenter.VAR_SN))), null, parsedMessage.get(MessageCenter.VAR_DATA), false);
				}
			}		
		} while (parsedMessage!=null && !stopRequesting);
		
		return (atLeastOneAccepted && !stopRequesting);
	}
	
	/**
	 * check if the picture that we are receiving has been changed yet 
	 * @param parsedMessage
	 * @return
	 */
	private boolean checkExpiartionOfPicMsg(HashMap<String, String> parsedMessage){
		String peerName = parsedMessage.get(MessageCenter.VAR_PEERNAME);
		String acceptedPicVer = parsedMessage.get(MessageCenter.VAR_PICVERSION);
		String currPicVer = DataBase.getInstance().findTuple(peerName).getPicVersion();
		
		return !(DateStringConverter.compare2Dates(acceptedPicVer, currPicVer) == 0);
	}
	
	/**
	 * 
	 * this method creates a DBTuple entity from an entry message and returns it
	 */

	public DBTuple createDBTupleFromEntryMessage(HashMap<String, String> entryMessage){
		DBTuple tuple = new DBTuple();
		tuple.setDbVersion(entryMessage.get(MessageCenter.VAR_DBVERSION));
		tuple.setPeerVersion(entryMessage.get(MessageCenter.VAR_PEERVERSION));
		tuple.setPeerName(entryMessage.get(MessageCenter.VAR_PEERNAME));
		tuple.setIp(entryMessage.get(MessageCenter.VAR_IP));
		tuple.setTcpPort(entryMessage.get(MessageCenter.VAR_TCP));
		tuple.setUdpPort(entryMessage.get(MessageCenter.VAR_UDP));
		tuple.setPhoneNum(entryMessage.get(MessageCenter.VAR_PHONE));
		tuple.setEmailAddress(entryMessage.get(MessageCenter.VAR_EMAIL));
		tuple.setLastConnectionTime(entryMessage.get(MessageCenter.VAR_LASTCONTIME));
		tuple.setStatus(entryMessage.get(MessageCenter.VAR_STATUS));
		tuple.setPicSize(entryMessage.get(MessageCenter.VAR_PICSIZE));
		tuple.setPicVersion(entryMessage.get(MessageCenter.VAR_PICVERSION));
		
		return tuple;
	}
	
	private HashMap<Integer, String> mapNewRequestsToSerials(String newListRequest){
		String[] fragments = newListRequest.split(",");
		HashMap<Integer,String> serialToFragmentNumMap = new HashMap<Integer,String>();
		
		for (int i=1; i<=fragments.length; ++i){
			serialToFragmentNumMap.put(i, fragments[i-1]);
		}
		
		return serialToFragmentNumMap;
	}

	public long getUniqueID(){
		return uniqueID;
	}
	
	
}
